# frozen_string_literal: true

require "shellwords"
require "rake/testtask"

task default: :test

ENV["RAILS_MINITEST_PLUGIN"] = "true"

desc "Run all unit tests"
task test: "test:isolated"

namespace :test do
  task :isolated do
    estimated_duration = {
      "test/application/test_runner_test.rb" => 179,
      "test/application/assets_test.rb" => 20,
      "test/application/rake/migrations_test.rb" => 90,
      "test/generators/scaffold_generator_test.rb" => 78,
      "test/generators/plugin_test_runner_test.rb" => 73,
      "test/application/test_test.rb" => 25,
      "test/application/configuration_test.rb" => 106,
      "test/generators/app_generator_test.rb" => 150,
      "test/application/rake/dbs_test.rb" => 82,
      "test/application/rake_test.rb" => 33,
      "test/generators/plugin_generator_test.rb" => 37,
      "test/railties/engine_test.rb" => 39,
      "test/generators/scaffold_controller_generator_test.rb" => 27,
      "test/railties/generators_test.rb" => 25,
      "test/application/console_test.rb" => 20,
      "test/engine/commands_test.rb" => 17,
      "test/application/routing_test.rb" => 21,
      "test/application/mailer_previews_test.rb" => 20,
      "test/application/rake/multi_dbs_test.rb" => 100,
      "test/application/asset_debugging_test.rb" => 5,
      "test/application/bin_setup_test.rb" => 13,
      "test/engine/test_test.rb" => 16,
      "test/application/runner_test.rb" => 13,
      "test/commands/routes_test.rb" => 7,
      "test/application/initializers/i18n_test.rb" => 8,
      "test/application/middleware/cache_test.rb" => 7,
      "test/application/middleware_test.rb" => 10,
      "test/generators/test_runner_in_engine_test.rb" => 7,
      "test/application/initializers/frameworks_test.rb" => 18,
      "test/application/middleware/exceptions_test.rb" => 12,
      "test/application/content_security_policy_test.rb" => 5,
    }
    estimated_duration.default = 1

    dash_i = [
      "test",
      "lib",
      "../activesupport/lib",
      "../actionpack/lib",
      "../actionview/lib",
      "../activemodel/lib"
    ].map { |dir| File.expand_path(dir, __dir__) }

    dash_i.reverse_each do |x|
      $:.unshift(x) unless $:.include?(x)
    end
    $-w = true

    require "bundler/setup" unless defined?(Bundler)
    require "active_support"

    # Only generate the template app once.
    require_relative "test/isolation/abstract_unit"
    require "minitest/rails_plugin"

    failing_files = []

    dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",")
    test_options = ENV["TESTOPTS"].to_s.split(/[\s]+/)
    test_options << "--profile" if ENV["BUILDKITE"]

    test_patterns = dirs.map { |dir| "test/#{dir}/*_test.rb" }
    test_files = Dir[*test_patterns].select do |file|
      !file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/")
    end

    if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
      n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
      m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i

      buckets = Array.new(m) { [] }
      allocations = Array.new(m) { 0 }
      test_files.sort_by { |file| [-estimated_duration[file], file] }.each do |file|
        idx = allocations.index(allocations.min)
        buckets[idx] << file
        allocations[idx] += estimated_duration[file]
      end

      puts "Running #{buckets[n].size} of #{test_files.size} test files, estimated duration #{allocations[n]}s"

      test_files = buckets[n]
    end

    output_file = Tempfile.new("railties_test_reporter")

    test_files.each do |file|
      puts "--- #{file}"
      fake_command = Shellwords.join([
        FileUtils::RUBY,
        "-w",
        *dash_i.map { |dir| "-I#{Pathname.new(dir).relative_path_from(Pathname.pwd)}" },
        file,
      ])
      puts fake_command

      if Process.respond_to?(:fork)
        # We could run these in parallel, but pretty much all of the
        # railties tests already run in parallel, so ¯\_(⊙︿⊙)_/¯
        Process.waitpid fork {
          ENV["RAILTIES_OUTPUT_FILE"] = output_file.path
          ARGV.clear.concat test_options
          Rake.application = nil

          require "../tools/strict_warnings"
          load file
        }
      else
        Process.wait spawn(fake_command)
      end

      unless $?.success?
        failing_files << file
        puts "^^^ +++"
      end
    end

    puts "+++ All tests completed"

    if ENV["BUILDKITE"]
      TestResult = Struct.new(:NAME, :failures, :assertions, :klass, :time, :source_location, :location)
      profile = Minitest::ProfileReporter.new($stdout, profile: 10)
      output_file.rewind
      output_file.each_line do |result|
        data = JSON.parse(result)
        profile.results << TestResult.new(*data.values)
      end

      profile.summary
    end

    unless failing_files.empty?
      puts "+++"
      puts
      puts "Failed in:"
      failing_files.each do |file|
        puts "  #{file}"
      end
      puts

      exit 1
    end
  ensure
    output_file&.close
    output_file&.unlink
  end
end

Rake::TestTask.new("test:regular") do |t|
  t.libs << "test" << "#{__dir__}/../activesupport/lib"
  t.pattern = "test/**/*_test.rb"
  t.warning = true
  t.verbose = true
  t.options = "--profile" if ENV["BUILDKITE"]
  t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
